home *** CD-ROM | disk | FTP | other *** search
- /*
- * For legal stuff see the file COPYRIGHT
- */
- #import <stdio.h>
- #import <time.h>
- #import <bsd/libc.h>
- #import <sys/file.h>
- #import <sys/types.h>
- #import <sys/stat.h>
- #import <objc/NXBundle.h>
- #import <appkit/appkit.h>
- #import "Controller.h"
- #import "Invoice.h"
- #import "Session.h"
- #import "Expense.h"
- #import "Preferences.h"
- #import "createPath.h"
-
- #define EXPENSE "Expense"
- #define DETAIL "Detail"
- #define INVOICE "Invoice"
- #define TEMPLATE_TYPE "rtf"
-
- #define MONTH_SWITCH_DAY 20
-
-
- @interface Invoice(PRIVATE)
- - (void)copyTemplateIfNeeded:(const char *)template;
- - (FILE *)openTemplate:(const char *)template;
- - (FILE *)openOutput:(const char *)path;
- - (void)filterExpenses:(FILE *)in to:(FILE *)out;
- - (void)filterDetail:(FILE *)in to:(FILE *)out;
- - (void)filter:(FILE *)in to:(FILE *)out;
- - (const char *)translate:(const char *)token;
- - (BOOL)filterBuffer:(char *)ptr to:(FILE *)out
- match:(const char *)target;
- - (const char *)templatePath:(const char *)template;
- - (const char *)createFilename:(const char *)type;
- - (int)nextInvoiceNumber;
- - generateInvoice;
- - generateDetail;
- - generateExpenses;
- - (int)likelyMonth:(struct tm *)tm;
-
- - (const char *)number; /* invoice number */
- - (const char *)today; /* today's date */
- - (const char *)thisMonth; /* the likely current invoicing month */
- - (const char *)contact; /* person at client */
- - (const char *)client; /* client name */
- - (const char *)street; /* street address */
- - (const char *)city;
- - (const char *)state;
- - (const char *)zip;
- - (const char *)enddate; /* end of invoice period */
- - (const char *)startdate; /* start of invoice period */
- - (const char *)billings; /* total hourly billings */
- - (const char *)expenses; /* total expenses */
- - (const char *)total; /* billings + expenses */
- - (const char *)hours; /* total hours worked for invoice period */
- - (const char *)rate; /* hourly rate */
- - (const char *)date; /* date of a session or expense */
- - (const char *)length; /* length of a session (in hours) */
- - (const char *)amount; /* expense amount */
- - (const char *)description; /* description of session task or expense */
-
- - (const char *)myName; /* consultant's personal information, from prefs */
- - (const char *)myCompany;
- - (const char *)myStreet;
- - (const char *)myCity;
- - (const char *)myState;
- - (const char *)myZip;
- - (const char *)myPhone;
- - (const char *)myFax;
- - (const char *)myEmail;
- @end
-
-
- @implementation Invoice
-
-
- - initTemplateDir:(const char *)dir
- {
- [super init];
-
- templateDir = NXCopyStringBuffer(dir);
-
- if ( createPath( templateDir, DIRMODE ) != PathCreationOk ) {
- NXRunAlertPanel( [NXApp name], "Cannot create path `%s'",
- "Harumph.", NULL, NULL, templateDir );
- return nil;
- }
-
- /*
- * If the templates are not found in the templateDir,
- * copy them there from the app wrapper.
- */
- [self copyTemplateIfNeeded:EXPENSE];
- [self copyTemplateIfNeeded:INVOICE];
- [self copyTemplateIfNeeded:DETAIL];
-
- return self;
- }
-
- - free
- {
- if ( templateDir ) {
- free(templateDir);
- templateDir = NULL;
- }
- return [super free];
- }
-
- - (BOOL)editTemplate:(const char *)template
- {
- const char *path = [self templatePath:template];
- return [[Application workspace] openFile:path];
- }
-
- - (void)generate:(List *)clientList
- {
- int i, count = [clientList count];
- int sessionCount, expenseCount;
-
- for ( i = 0; i < count; i++ ) {
-
- info = [clientList objectAt:i];
- sessionCount = [info sessionCount];
- expenseCount = [info expenseCount];
-
- /* Don't generate an invoice if no work was done */
- if ( sessionCount == 0 && expenseCount == 0 )
- continue;
-
- /* get (and increment) the invoice number */
- number = [self nextInvoiceNumber];
-
- [self generateInvoice];
-
- if ( sessionCount > 0 )
- [self generateDetail];
-
- if ( expenseCount > 0 )
- [self generateExpenses];
- }
- }
-
- @end
-
-
- @implementation Invoice(PRIVATE)
-
- static char *Tokens[] = {
- "number",
- "today",
- "contact",
- "client",
- "street",
- "city",
- "state",
- "zip",
- "startdate",
- "enddate",
- "billings",
- "expenses",
- "total",
- "hours",
- "rate",
- "date",
- "length",
- "amount",
- "description",
- "myName",
- "myCompany",
- "myStreet",
- "myCity",
- "myState",
- "myZip",
- "myPhone",
- "myFax",
- "myEmail",
- 0,
- };
-
- #define START '['
- #define END ']'
- #define MAX_LINE 500
- #define MAX_TOKEN 20
-
- /*
- * Determine the value of the token, or return NULL if
- * not a known token.
- */
- - (const char *)translate:(const char *)token
- {
- char **ptr;
-
- for ( ptr = Tokens; *ptr; ptr++ ) {
- if ( strcmp( *ptr, token ) == 0 ) {
- SEL sel = sel_getUid(token);
-
- if ( [self respondsTo:sel] )
- return (const char *)[self perform:sel];
- else
- return NULL; /* found string, but don't respond */
- }
- }
- return NULL;
- }
-
- /*
- * Translate the line pointed to by 'ptr', writing the result to the
- * stream 'out'. Return 1 if the given token occurred in this line.
- */
- - (BOOL)filterBuffer:(char *)ptr to:(FILE *)output
- match:(const char *)target
- {
- char c, *tok, token[MAX_TOKEN + 1];
- const char *value;
- int count;
- BOOL matched = NO;
-
- for ( ; *ptr ; ptr++ ) {
- if ( *ptr == START ) {
-
- for (count = 0, tok = token;
- count < MAX_TOKEN && (c = *++ptr) && NXIsAlpha(c);
- count++ )
- *tok++ = c;
-
- *tok = '\0';
-
- if ( c == END ) { /* got a token */
- if ( (value = [self translate:token]) == NULL ) /* it was unknown */
- fprintf(output, "%c%s%c", START, token, END); /* pass it thru */
- else {
- fprintf(output, "%s", value); /* use its value */
- if ( target && strcmp( token, target ) == 0 )
- matched = YES;
- }
- } else if ( !NXIsAlpha(c) ) /* bad character */
- fprintf(output, "%c%s%c", START, token, c); /* pass it thru */
- else /* it was too long */
- fprintf(output, "%c%s", START, token); /* pass it thru */
- } else
- fputc( *ptr, output );
- }
- return matched;
- }
-
- /*
- * Straight translation - used to generate invoices.
- */
- - (void)filter:(FILE *)input to:(FILE *)output
- {
- char buf[MAX_LINE + 1];
-
- while ( fgets( buf, MAX_LINE, input ) )
- [self filterBuffer:buf to:output match:NULL];
- }
-
- /*
- * Expense generator - loops over Expense records when it
- * finds the {description} token in a line.
- */
- - (void)filterExpenses:(FILE *)input to:(FILE *)output
- {
- char buf[MAX_LINE + 1];
-
- while ( fgets( buf, MAX_LINE, input ) ) {
- /*
- * When we find {description}, treat the current buf as
- * the template for all expenses.
- */
- if ( [self filterBuffer:buf to:output match:"description"] ) {
- int i, count = [info expenseCount];
-
- for ( i = 0; i < count; i++ ) {
- expense = (Expense *)[info expenseAt:i];
- [self filterBuffer:buf to:output match:NULL];
- }
- expense = nil;
- }
- }
- }
-
- /*
- * Detail generator - loops over Session records when it
- * finds the {description} token in a line.
- */
- - (void)filterDetail:(FILE *)input to:(FILE *)output
- {
- char buf[MAX_LINE + 1];
-
- while ( fgets( buf, MAX_LINE, input ) ) {
- /*
- * When we find {description}, treat the current buf as
- * the template for all sessions.
- */
- if ( [self filterBuffer:buf to:output match:"description"] ) {
- int i, count = [info sessionCount];
-
- for ( i = 0; i < count; i++ ) {
- session = (Session *)[info sessionAt:i];
- [self filterBuffer:buf to:output match:NULL];
- }
- session = nil;
- }
- }
- }
-
- /*
- * Intuit the most likely month that this invoice is for. If it is before
- * the 20th day of the month, assume it is for the prior month, otherwise
- * assume it is for the current month.
- */
- - (int)likelyMonth:(struct tm *)tm
- {
- int month = tm->tm_mon;
-
- if ( tm->tm_mday < MONTH_SWITCH_DAY )
- month = ( month == 0 ? 11 : month - 1 ); /* special for January */
-
- return month;
- }
-
- /*
- * Return the (likely) current invoicing month.
- */
- - (const char *)thisMonth
- {
- time_t now;
- struct tm *tm;
- static char *months[] = {
- "jan", "feb", "mar", "apr",
- "may", "jun", "jul", "aug", "sep",
- "oct", "nov", "dec"
- };
-
- time(&now);
- tm = localtime(&now);
-
- return months[[self likelyMonth:tm]];
- }
-
- /***********************************************************************
- * Token Translation Methods
- ***********************************************************************/
- - (const char *)number
- {
- static char buf[20];
- sprintf( buf, "%d", number );
- return buf;
- }
-
- /*
- * Return today's date as a string
- */
- - (const char *)today
- {
- time_t now;
- struct tm *tm;
- static char *months[] = {
- "January", "February", "March", "April",
- "May", "June", "July", "August", "September",
- "October", "November", "December"
- };
- static char buf[80];
-
- time(&now);
- tm = localtime(&now);
- sprintf(buf, "%s %d, 19%d",
- months[tm->tm_mon], tm->tm_mday, tm->tm_year );
- return buf;
- }
-
- - (const char *)contact
- {
- return info ? [info contactName] : "";
- }
-
- - (const char *)client
- {
- return info ? [info clientName] : "";
- }
-
- - (const char *)street
- {
- return info ? [info street] : "";
- }
-
- - (const char *)city
- {
- return info ? [info city] : "";
- }
-
- - (const char *)state
- {
- return info ? [info addrState] : "";
- }
-
- - (const char *)zip
- {
- return info ? [info zipCode] : "";
- }
-
- - (const char *)enddate
- {
- int month;
- time_t now;
- struct tm *tm;
- static char buf[80];
- static int lengths[] = { /* number of days per month */
- 31, 28, 31, 30, /* gloss over leap years... */
- 31, 30, 31, 31,
- 30, 31, 30, 31
- };
-
- time(&now);
- tm = localtime(&now);
- month = [self likelyMonth:tm];
-
- sprintf( buf, "%02d/%02d/%02d", month + 1, lengths[month], tm->tm_year );
- return buf;
- }
-
- /*
- * Intuit the most likely start date for this invoice. If it is before
- * the 20th day of the month, assume it is for the prior month, otherwise
- * assume it is for the current month.
- */
- - (const char *)startdate
- {
- int month;
- time_t now;
- struct tm *tm;
- static char buf[80];
-
- time(&now);
- tm = localtime(&now);
- month = [self likelyMonth:tm];
-
- sprintf( buf, "%02d/01/%02d", month + 1, tm->tm_year );
- return buf;
- }
-
- - (const char *)billings
- {
- static char buf[20];
-
- if ( info ) {
- commafyDouble( [info totalBillable], buf );
- return buf;
- }
-
- return "";
- }
-
- - (const char *)expenses
- {
- static char buf[20];
-
- if ( info ) {
- commafyDouble( [info totalExpenses], buf );
- return buf;
- }
-
- return "";
- }
-
- - (const char *)total
- {
- static char buf[20];
-
- if ( info ) {
- commafyDouble( [info totalBillable] + [info totalExpenses], buf );
- return buf;
- }
-
- return "";
- }
-
- - (const char *)hours
- {
- static char buf[20];
-
- if ( info ) {
- sprintf( buf, "%1.2f", [info totalHours] );
- return buf;
- }
- return "";
- }
-
- - (const char *)rate
- {
- static char buf[20];
-
- if ( info ) {
- sprintf( buf, "%1.2f", [info hourlyRate] );
- return buf;
- }
- return "";
- }
-
- - (const char *)date
- {
- if ( session )
- return [session startDateString];
-
- if ( expense )
- return [expense dateString];
-
- return "";
- }
-
- - (const char *)amount
- {
- static char buf[20];
-
- if ( !expense )
- return "";
-
- commafyDouble( [expense amount], buf );
- return buf;
- }
-
- - (const char *)length
- {
- static char buf[20];
-
- if ( !session )
- return "";
-
- sprintf( buf, "%1.2f", [session hours] );
- return buf;
- }
-
- - (const char *)description
- {
- if ( expense )
- return [expense description];
-
- if ( session )
- return [session description];
-
- return "";
- }
-
- /*
- * Consultant information taken from Preferences
- */
- - (const char *)myName
- {
- return [[Preferences new] myName];
- }
-
- - (const char *)myCompany
- {
- return [[Preferences new] myCompany];
- }
-
- - (const char *)myStreet
- {
- return [[Preferences new] myStreet];
- }
-
- - (const char *)myCity
- {
- return [[Preferences new] myCity];
- }
-
- - (const char *)myState
- {
- return [[Preferences new] myState];
- }
-
- - (const char *)myZip
- {
- return [[Preferences new] myZip];
- }
-
- - (const char *)myPhone
- {
- return [[Preferences new] myPhone];
- }
-
- - (const char *)myFax
- {
- return [[Preferences new] myFax];
- }
-
- - (const char *)myEmail
- {
- return [[Preferences new] myEmail];
- }
-
-
- - (const char *)templatePath:(const char *)template
- {
- static char path[FILENAME_MAX + 1];
- sprintf( path, "%s/%sTemplate.%s", templateDir, template, TEMPLATE_TYPE );
- return path;
- }
-
- /*
- * If the named template file is not found in the templateDir,
- * copy the one from the app wrapper over there.
- */
- - (void)copyTemplateIfNeeded:(const char *)template
- {
- FILE *fp;
- const char *templatePath;
- char templateSrc[FILENAME_MAX + 1];
- char name[FILENAME_MAX + 1];
-
- templatePath = [self templatePath:template];
-
- /* assume it's ok if we can open it for reading */
- if ( (fp = fopen( templatePath, "r" )) != NULL ) {
- fclose(fp);
- return; /* the file is already there */
- }
-
- sprintf( name, "%sTemplate", template );
-
- if ( ! [[NXBundle mainBundle] getPath:templateSrc
- forResource:name ofType:TEMPLATE_TYPE] ) {
- NXRunAlertPanel( [NXApp name], "Can't find %s.%s in app wrapper",
- "What Bogosity!", NULL, NULL, name, TEMPLATE_TYPE );
- return;
- }
-
- copyFile( templateSrc, templatePath );
- }
-
- /*
- * Open a template file for reading.
- */
- - (FILE *)openTemplate:(const char *)template
- {
- FILE *fp;
- const char *path = [self templatePath:template];
-
- if ( ! (fp = fopen( path, "r" )) )
- NXRunAlertPanel( [NXApp name], "Can't open `%s' for reading",
- "Dang!", NULL, NULL, path );
-
- return fp;
- }
-
- /*
- * Take out blanks and punctuation characters to create a reasonable
- * file name. NB: Returns a pointer to a local static buffer
- */
- - (const char *)createFilename:(const char *)type
- {
- char c, *ptr;
- const char *dir = GET_DEFAULT( INVOICE_DIR );
- const char *src = [info shortName];
- static char buf[MAXPATHLEN];
-
- sprintf( buf, "%s/", dir ) ;
- ptr = buf + strlen(buf);
-
- while ( (c = *src++) != '\0' )
- if ( NXIsAlpha(c) ) /* don't copy punctuation */
- *ptr++ = ( NXIsUpper(c) ? NXToLower(c) : c );
-
- sprintf( ptr, ".%d.%s.%s.rtf", number, [self thisMonth], type );
- return buf;
- }
-
- /*
- * Open a file of the given type for writing
- */
- - (FILE *)openOutput:(const char *)type
- {
- FILE *fp;
- const char *path = [self createFilename:type];
-
- if ( ! (fp = fopen( path, "w" )) )
- NXRunAlertPanel( [NXApp name], "Can't open `%s' for writing",
- "Drat.", NULL, NULL, path );
-
- return fp;
- }
-
- /*
- * Read the current "next invoice number" from the defaults database
- * increment and return the number, after storing the value back.
- */
- - (int)nextInvoiceNumber
- {
- char num[10];
- int value;
-
- value = atoi(GET_DEFAULT( INVOICE_NUM ));
- sprintf( num, "%d", ++value );
- PUT_DEFAULT( INVOICE_NUM, num );
-
- return value; /* return the incremented value */
- }
-
- /*
- * Generate an expense report data file for any customer that had
- * expenses for this period.
- */
- - generateExpenses
- {
- FILE *in, *out;
-
- if ( !( in = [self openTemplate:EXPENSE]) || !( out = [self openOutput:"exp"] ))
- return nil;
-
- [self filterExpenses:in to:out];
-
- fclose( in );
- fclose( out );
-
- return self;
- }
-
- - generateDetail
- {
- FILE *in, *out;
-
- if ( !( in = [self openTemplate:DETAIL]) || !( out = [self openOutput:"dtl"] ))
- return nil;
-
- [self filterDetail:in to:out];
-
- fclose( in );
- fclose( out );
-
- return self;
- }
-
- - generateInvoice
- {
- FILE *in, *out;
-
- if ( !( in = [self openTemplate:INVOICE]) || !( out = [self openOutput:"inv"] ))
- return nil;
-
- [self filter:in to:out];
-
- fclose( in );
- fclose( out );
-
- return self;
- }
-
- @end
-